/** * Copyright (c) 2013, Joyent, Inc. All rights reserved. */ package io.urmia.auth.joyent; import java.io.FileReader; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.security.*; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Date; import java.util.TimeZone; import io.urmia.auth.CryptoException; import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.openssl.PEMDecryptorProvider; import org.bouncycastle.openssl.PEMEncryptedKeyPair; import org.bouncycastle.openssl.PEMKeyPair; import org.bouncycastle.openssl.PEMParser; import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter; import org.bouncycastle.openssl.jcajce.JcePEMDecryptorProviderBuilder; import org.bouncycastle.util.encoders.Base64; import com.google.api.client.http.HttpRequest; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Joyent HTTP authorization signer. This adheres to the specs of the node-http-signature spec. * * @author Yunong Xiao */ public class HttpSigner { private static final Logger LOG = LoggerFactory.getLogger(HttpSigner.class); private static final DateFormat DATE_FORMAT = new SimpleDateFormat("EEE MMM d HH:mm:ss yyyy zzz"); private static final String AUTHZ_HEADER = "Signature keyId=\"/%s/keys/%s\",algorithm=\"rsa-sha256\",signature=\"%s\""; private static final String AUTHZ_SIGNING_STRING = "date: %s"; private static final String AUTHZ_PATTERN = "signature=\""; static final String SIGNING_ALGORITHM = "SHA256WithRSAEncryption"; static { Security.addProvider(new BouncyCastleProvider()); } /** * Returns a new {@link HttpSigner} instance that can be used to sign and verify requests according to the * joyent-http-signature spec. * * @see <a href="http://github.com/joyent/node-http-signature/blob/master/http_signing.md">node-http-signature</a> * @param keyPath * The path to the rsa key on disk. * @param fingerPrint * The fingerprint of the rsa key. * @param login * The login of the user account. * @return An instance of {@link HttpSigner}. * @throws IOException * If the key is invalid. */ public static final HttpSigner newInstance(String keyPath, String fingerPrint, String login) throws IOException { return new HttpSigner(keyPath, fingerPrint, login); } // final KeyPair keyPair_; private final PrivateKey privateKey; private final String login_; private final String fingerPrint_; /** * @param keyPath * @throws IOException */ private HttpSigner(String keyPath, String fingerprint, String login) throws IOException { LOG.debug(String.format("initializing HttpSigner with keypath: %s, fingerprint: %s, login: %s", keyPath, fingerprint, login)); fingerPrint_ = fingerprint; login_ = login; // keyPair_ = getKeyPair(keyPath); privateKey = readPrivateKey(keyPath, ""); } private PrivateKey readPrivateKey(String privateKeyPath, String keyPassword) throws IOException { FileReader fileReader = new FileReader(privateKeyPath); PEMParser keyReader = new PEMParser(fileReader); JcaPEMKeyConverter converter = new JcaPEMKeyConverter(); PEMDecryptorProvider decryptionProv = new JcePEMDecryptorProviderBuilder().build(keyPassword.toCharArray()); Object keyPair = keyReader.readObject(); PrivateKeyInfo keyInfo; if (keyPair instanceof PEMEncryptedKeyPair) { PEMKeyPair decryptedKeyPair = ((PEMEncryptedKeyPair) keyPair).decryptKeyPair(decryptionProv); keyInfo = decryptedKeyPair.getPrivateKeyInfo(); } else { keyInfo = ((PEMKeyPair) keyPair).getPrivateKeyInfo(); } keyReader.close(); return converter.getPrivateKey(keyInfo); } /** * @param keyPath * @return * @throws IOException */ /* private final KeyPair getKeyPair(String keyPath) throws IOException { BufferedReader br = new BufferedReader(new FileReader(keyPath)); Security.addProvider(new BouncyCastleProvider()); PemReader pemReader = new PemReader(br); KeyPair kp = (KeyPair) pemReader.readObject(); pemReader.close(); return kp; } */ /** * Sign an {@link HttpRequest}. * * @param request * The {@link HttpRequest} to sign. * @throws CryptoException * If unable to sign the request. */ public final void signRequest(HttpRequest request) throws CryptoException { LOG.debug("signing request: " + request.getHeaders()); String date = request.getHeaders().getDate(); if (date == null) { Date now = Calendar.getInstance(TimeZone.getTimeZone("UTC")).getTime(); date = DATE_FORMAT.format(now); LOG.debug("setting date header: " + date); request.getHeaders().setDate(date); } try { Signature sig = Signature.getInstance(SIGNING_ALGORITHM); sig.initSign(/*keyPair_.getPrivate()*/privateKey); String signingString = String.format(AUTHZ_SIGNING_STRING, date); sig.update(signingString.getBytes("UTF-8")); byte[] signedDate = sig.sign(); byte[] encodedSignedDate = Base64.encode(signedDate); String authzHeader = String.format(AUTHZ_HEADER, login_, fingerPrint_, new String(encodedSignedDate)); request.getHeaders().setAuthorization(authzHeader); } catch (NoSuchAlgorithmException e) { throw new CryptoException("invalid algorithm", e); } catch (InvalidKeyException e) { throw new CryptoException("invalid key", e); } catch (SignatureException e) { throw new CryptoException("invalid signature", e); } catch (UnsupportedEncodingException e) { throw new CryptoException("invalid encoding", e); } } /* public final boolean verifyRequest(HttpRequest request) throws CryptoException { LOG.debug("verifying request: " + request.getHeaders()); String date = request.getHeaders().getDate(); if (date == null) { throw new CryptoException("no date header in request"); } date = String.format(AUTHZ_SIGNING_STRING, date); try { Signature verify = Signature.getInstance(SIGNING_ALGORITHM); verify.initVerify(keyPair_.getPublic()); String authzHeader = request.getHeaders().getAuthorization(); int startIndex = authzHeader.indexOf(AUTHZ_PATTERN); if (startIndex == -1) { throw new CryptoException("invalid authorization header " + authzHeader); } String encodedSignedDate = authzHeader.substring(startIndex + AUTHZ_PATTERN.length(), authzHeader.length() - 1); byte[] signedDate = Base64.decode(encodedSignedDate.getBytes("UTF-8")); verify.update(date.getBytes("UTF-8")); return verify.verify(signedDate); } catch (NoSuchAlgorithmException e) { throw new CryptoException("invalid algorithm", e); } catch (InvalidKeyException e) { throw new CryptoException("invalid key", e); } catch (SignatureException e) { throw new CryptoException("invalid signature", e); } catch (UnsupportedEncodingException e) { throw new CryptoException("invalid encoding", e); } } */ }